package org.springframework.boot.logging;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.springframework.boot.logging.CorrelationIdFormatter.Part;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

public final class CorrelationIdFormatter {

	/**
	 * Default {@link CorrelationIdFormatter}.
	 */
	public static final CorrelationIdFormatter DEFAULT = CorrelationIdFormatter.of("traceId(32),spanId(16)");

	private final List<Part> parts;
	private final String blank;

	private CorrelationIdFormatter(List<Part> parts) {
		this.parts = parts;
		StringBuilder blankBuilder = new StringBuilder();
		for (Part part : parts) {
			blankBuilder.append(part.blank());
		}
		this.blank = String.format("[%s] ", blankBuilder.toString());
	}

	public String format(UnaryOperator<String> resolver) {
		StringBuilder result = new StringBuilder();
		formatTo(resolver, result);
		return result.toString();
	}

	public void formatTo(UnaryOperator<String> resolver, Appendable appendable) {
		// Predicate definition is the same in Java 8
		try {
			if (parts.stream().anyMatch(part -> StringUtils.hasLength(resolver.apply(part.name())))) {
				appendable.append('[');
				for (Iterator<Part> iterator = parts.iterator(); iterator.hasNext();) {
					appendable.append(iterator.next().resolve(resolver));
					if (iterator.hasNext()) {
						appendable.append('-');
					}
				}
				appendable.append("] ");
			} else {
				appendable.append(this.blank);
			}
		} catch (IOException ex) {
			throw new UncheckedIOException(ex);
		}
	}

	@Override
	public String toString() {
		return parts.stream().map(Part::toString).collect(Collectors.joining(","));
	}

    public static CorrelationIdFormatter of(String spec) {
        try {
            return (!StringUtils.hasText(spec)) ? DEFAULT : of(spec.split(","));
        } catch (Exception ex) {
            throw new IllegalStateException("Unable to parse correlation formatter spec '" + spec + "'", ex);
        }
    }

    public static CorrelationIdFormatter of(String[] spec) {
        return of(spec != null ? Arrays.asList(spec) : Collections.emptyList());
    }

    public static CorrelationIdFormatter of(Collection<String> spec) {
        if (spec == null || spec.size()<1) {
            return DEFAULT;
        }
        List<Part> parts = new ArrayList<>();
        for (String s : spec) {
            parts.add(Part.of(s));
        }
        return new CorrelationIdFormatter(parts);
    }

	static class Part {
		private static final Pattern pattern = Pattern.compile("^(.+?)\\((\\d+)\\)$");

		private final String name;
		private final int length;

		Part(String name, int length) {
			this.name = name;
			this.length = length;
		}

		String resolve(UnaryOperator<String> resolver) {
			String resolved = resolver.apply(name());
			if (resolved == null) {
				return blank();
			}
			int padding = length - resolved.length();
			StringBuilder paddingBuilder = new StringBuilder();
			for (int i = 0; i < padding; i++) {
				paddingBuilder.append(" ");
			}
			return padding <= 0 ? resolved : resolved + paddingBuilder.toString();
		}

		String blank() {
			StringBuilder blankBuilder = new StringBuilder();
			for (int i = 0; i < this.length; i++) {
				blankBuilder.append(" ");
			}
			return blankBuilder.toString();
		}

		@Override
		public String toString() {
			return String.format("%s(%s)", name, length);
		}

		static Part of(String part) {
			Matcher matcher = pattern.matcher(part.trim());
			String f = String.format("Invalid specification part '%s'", part);
			Assert.state(matcher.matches(), f);
			String name = matcher.group(1);
			int length = Integer.parseInt(matcher.group(2));
			return new Part(name, length);
		}

		public String name() {
			return name;
		}

		public int length() {
			return length;
		}
	}
}